今天將會透過 Lession 14: Build an Oracle 練習製作 Oracle Contract
假設今天要製作一個去中心化金融應用,這個應用讓使用者能從 Contract 把等值於美金換成 1 Ether。
為了要完成這個這個應用,應用 Contract 必須要能知道當下 1 Ether 值多少美金。
要查詢當下 Ether 對應到美金的匯率可以透過 Biance 交易所的 API 去查
然而, Contract 本身無法直接與外界 api 做互動
所以需要一個 Oracle 來當作外界資料來源。
Oracle 是 Smart Contract 用來與外界呼叫的一種機制。
其流程如下
npm init -y
or
yarn init -y
npm i truffle openzeppelin-solidity loom-js loom-truffle-provider bn.js axios
or
yarn add truffle openzeppelin-solidity loom-js loom-truffle-provider bn.js axios
mkdir oracle
在 oracle 內初始化 Truffle
cd oracle; npx truffle init; cd ..
mkdir caller
在 caller 內初始化 Truffle
cd caller; npx truffle init; cd ..
檢查資料夾結構透過 tree 指令如下
tree -L 2 -I node_modules
.
├── caller
│ ├── contracts
│ ├── migrations
│ ├── test
│ └── truffle-config.js
├── oracle
│ ├── contracts
│ ├── migrations
│ ├── test
│ └── truffle-config.js
└── package.json
在開始實作 Oracle Contract 之前
首先要來看一下呼叫 Oracle Contract 的 Caller Contract
為了要能與Oracle Contract 互動,Caller Contract 必須要俱備以下資訊
已知到一旦 Oracle Contract 一發佈到鏈上,要更新就只能重新發佈
為了能夠讓 Caller Contract 能夠在 Oracle Contract 更新時不被影響到要重新發佈
需要透過建立個 function 來更新 Oracle Contrat 的 address
address private oracleAddress;
oracleAddress = _oracleInstanceAddress;
pragma solidity 0.5.0;
contract CallerContract {
// start here
address private oracleAddress;
function setOracleInstanceAddress(address _oracleInstanceAddress) public {
oracleAddress = _oracleInstanceAddress;
}
}
介面類似於 Contract,但只是用來宣告 functions signature 卻不實作。且所有 function 接需要為 external
舉例來說:假設有一個 FastFood Contract
如下
pragma solidity 0.5.0;
contract FastFood {
function makeSandwich(string calldata _fillingA, string calldata _fillingB) external {
//Make the sandwich
}
}
假設需要從另一個 PrepareLunch Contract 呼叫 makeSandSwich 就必須要定一個介面 FastFoodInterface.sol
如下
pragma solidity 0.5.0;
interface FastFoodInterface {
function makeSandwich(string calldata _fillingA, string calldata _fillingB) external;
}
然後在 PrepareLaunch 內引入介面
加入初始化的 FastFoodInterface 邏輯
fastFoodInstance = FastFoodInterface(_address);
然後就可以使用 fastFoodInstance 來呼教 makeSandwich
pragma solidity 0.5.0;
import "./FastFoodInterface.sol";
contract PrepareLunch {
FastFoodInterface private fastFoodInstance;
function instantiateFastFoodContract (address _address) public {
fastFoodInstance = FastFoodInterface(_address);
fastFoodInstance.makeSandwich("sliced ham", "pickled veggies");
}
}
pragma solidity 0.5.0;
//1. Import from the "./EthPriceOracleInterface.sol" file
import "./EthPriceOracleInterface.sol";
contract CallerContract {
// 2. Declare `EthPriceOracleInterface`
EthPriceOracleInterface private oracleInstance;
address private oracleAddress;
function setOracleInstanceAddress (address _oracleInstanceAddress) public {
oracleAddress = _oracleInstanceAddress;
//3. Instantiate `EthPriceOracleInterface`
oracleInstance = EthPriceOracleInterface(oracleAddress);
}
}
前面 setOracleInstanceAddress 因為設定成 public
代表其他 Contract 可以任意更改 Oracle Address
因此需要使用 OpenZepelin's Ownable Contract 來限制呼叫者限定只有 owner 才能去做執行
pragma solidity 0.5.0;
import "./EthPriceOracleInterface.sol";
// 1. import the contents of "openzeppelin-solidity/contracts/ownership/Ownable.sol"
import "openzeppelin-solidity/contracts/ownership/Ownable.sol";
contract CallerContract is Ownable { // 2. Make the contract inherit from `Ownable`
EthPriceOracleInterface private oracleInstance;
address private oracleAddress;
event newOracleAddressEvent(address oracleAddress);
// 3. On the next line, add the `onlyOwner` modifier to the `setOracleInstanceAddress` function definition
function setOracleInstanceAddress (address _oracleInstanceAddress) public onlyOwner {
oracleAddress = _oracleInstanceAddress;
oracleInstance = EthPriceOracleInterface(oracleAddress);
// 4. Fire `newOracleAddressEvent`
emit newOracleAddressEvent(oracleAddress);
}
}
接下來將講解 Oracle 更新 ETH 的價格流程
當 ETH 價格更新, Smart Contract 會呼叫 Oracle Contract s內 getLatestEthPrice 功能。
然而,因為是非同步的, getLatestEthPrice 並不會直接回傳該值。
取而代之,會回傳一個 request id 。
然後 Oracle Contract 會透過 Biance API 讀取當下的 ETH 價格,然後執行 Caller Contract 對外的 callback function
Caller Contract 透過 callback function 取得最新的 ETH 價格
每個 Dapp 的使用者都可以透過呼叫 Caller Contract 來發起更新 ETH 價格的 Request 。
因為 Caller Contract 無法在被呼叫時來即時處理這這些 Request 所以必須要使用一個結構來紀錄這些還沒有回應的 Request 。
這樣才能夠在之後取得當下 ETH 價格後,讓 callback 找到對應的 Request 作為回應。
可以使用一個叫作 myRequest 的 mapping 用來紀錄每個 requestID 對應的 Request 是否有回應過
mapping(uint256 => bool) myRequests;
pragma solidity 0.5.0;
import "./EthPriceOracleInterface.sol";
import "openzeppelin-solidity/contracts/ownership/Ownable.sol";
contract CallerContract is Ownable {
EthPriceOracleInterface private oracleInstance;
address private oracleAddress;
mapping(uint256=>bool) myRequests;
event newOracleAddressEvent(address oracleAddress);
event ReceivedNewRequestIdEvent(uint256 id);
function setOracleInstanceAddress (address _oracleInstanceAddress) public onlyOwner {
oracleAddress = _oracleInstanceAddress;
oracleInstance = EthPriceOracleInterface(oracleAddress);
emit newOracleAddressEvent(oracleAddress);
}
// Define the `updateEthPrice` function
function updateEthPrice() public {
uint256 id = oracleInstance.getLatestEthPrice();
myRequests[id] = true;
emit ReceivedNewRequestIdEvent(id);
}
}
呼叫 Binance API 是非同步的操作。所以 Caller Smart Contract 需要提供一個 callback function 來讓 Oracle Contract 呼叫。
以下是 callback 流程解說
delete myRequest[id];
uint256 ethPrice private;
event PriceUpdatedEvent(uint256 ethPrice, uint256 id);
require(myRequests[id], "This request is not in my pending list.");
delete myRequests[id];
pragma solidity 0.5.0;
import "./EthPriceOracleInterface.sol";
import "openzeppelin-solidity/contracts/ownership/Ownable.sol";
contract CallerContract is Ownable {
// 1. Declare ethPrice
uint256 private ethPrice;
EthPriceOracleInterface private oracleInstance;
address private oracleAddress;
mapping(uint256=>bool) myRequests;
event newOracleAddressEvent(address oracleAddress);
event ReceivedNewRequestIdEvent(uint256 id);
// 2. Declare PriceUpdatedEvent
event PriceUpdatedEvent(uint256 ethPrice, uint256 id);
function setOracleInstanceAddress (address _oracleInstanceAddress) public onlyOwner {
oracleAddress = _oracleInstanceAddress;
oracleInstance = EthPriceOracleInterface(oracleAddress);
emit newOracleAddressEvent(oracleAddress);
}
function updateEthPrice() public {
uint256 id = oracleInstance.getLatestEthPrice();
myRequests[id] = true;
emit ReceivedNewRequestIdEvent(id);
}
function callback(uint256 _ethPrice, uint256 _id) public {
// 3. Continue here
require(myRequests[_id], "This request is not in my pending list.");
ethPrice = _ethPrice;
delete myRequests[_id];
emit PriceUpdatedEvent(_ethPrice, _id);
}
}
在實作完 callback function 之後
必須要確保只有 Oracale Contract 才能去呼叫
可以透過檢查 msg.sender 是否等於 oracleAddress 來實作一個 onlyOracle modifier
require(msg.sender == oracleAddress, "You are not authorized to call this function.");
pragma solidity 0.5.0;
import "./EthPriceOracleInterface.sol";
import "openzeppelin-solidity/contracts/ownership/Ownable.sol";
contract CallerContract is Ownable {
uint256 private ethPrice;
EthPriceOracleInterface private oracleInstance;
address private oracleAddress;
mapping(uint256=>bool) myRequests;
event newOracleAddressEvent(address oracleAddress);
event ReceivedNewRequestIdEvent(uint256 id);
event PriceUpdatedEvent(uint256 ethPrice, uint256 id);
function setOracleInstanceAddress (address _oracleInstanceAddress) public onlyOwner {
oracleAddress = _oracleInstanceAddress;
oracleInstance = EthPriceOracleInterface(oracleAddress);
emit newOracleAddressEvent(oracleAddress);
}
function updateEthPrice() public {
uint256 id = oracleInstance.getLatestEthPrice();
myRequests[id] = true;
emit ReceivedNewRequestIdEvent(id);
}
function callback(uint256 _ethPrice, uint256 _id) public onlyOracle {
require(myRequests[_id], "This request is not in my pending list.");
ethPrice = _ethPrice;
delete myRequests[_id];
emit PriceUpdatedEvent(_ethPrice, _id);
}
modifier onlyOracle() {
// Start here
require(msg.sender == oracleAddress, "You are not authorized to call this function.");
_;
}
}
為了讓呼叫者可以紀錄 Request, getLatestEthPrice 首先需要計算 request id 還有為了避免 id 被人盜用,這個 id 必須很難預測。
為了難以預測也許會需要使用隨機數
隨機數的作法可以透過 kecca256 加入時間戳 now 與 randNounce 來實作如下
uint randNonce = 0;
uint modulus = 1000;
uint randomNumber = uint(keccak256(abi.encodePacked(now, msg.sender, randNonce))) % modulus;
這個隨機數產生了 0 - 999 之間的隨機數。
function getLatestEthPrice() public returns(uint256) {
}
pragma solidity 0.5.0;
import "openzeppelin-solidity/contracts/ownership/Ownable.sol";
import "./CallerContractInterface.sol";
contract EthPriceOracle is Ownable {
uint private randNonce = 0;
uint private modulus = 1000;
mapping(uint256=>bool) pendingRequests;
event GetLatestEthPriceEvent(address callerAddress, uint id);
event SetLatestEthPriceEvent(uint256 ethPrice, address callerAddress);
// Start here
function getLatestEthPrice() public returns(uint256) {
randNonce++;
uint id = uint(keccak256(abi.encodePacked(now, msg.sender, randNonce))) % modulus;
}
}
接下來必須實作一個簡單的系統來紀錄 pending request
可以透過 mapping 來紀錄這些 request
最後 getLatestEthPrice 會發送一個 event 來通知該 request id
pragma solidity 0.5.0;
import "openzeppelin-solidity/contracts/ownership/Ownable.sol";
import "./CallerContractInterface.sol";
contract EthPriceOracle is Ownable {
uint private randNonce = 0;
uint private modulus = 1000;
mapping(uint256=>bool) pendingRequests;
event GetLatestEthPriceEvent(address callerAddress, uint id);
event SetLatestEthPriceEvent(uint256 ethPrice, address callerAddress);
function getLatestEthPrice() public returns (uint256) {
randNonce++;
uint id = uint(keccak256(abi.encodePacked(now, msg.sender, randNonce))) % modulus;
// Start here
pendingRequests[id] = true;
emit GetLatestEthPriceEvent(msg.sender, id);
return id;
}
}
當透過 js 元件取得 ETH 價格後 ,最後會呼叫 setLatestEthPrice 這個 function 來傳送結果
setLatestEthPrice 主要會有以下參數
首先必須要確認這個 function 只有 owner 可以呼叫
然後確認 request id 是否合法
如果合法最後要從 pendingRequests 移除掉 id
require(pendingRequests[_id], "This request is not in my pending list.");
delete pendingRequests[id];
pragma solidity 0.5.0;
import "openzeppelin-solidity/contracts/ownership/Ownable.sol";
import "./CallerContractInterface.sol";
contract EthPriceOracle is Ownable {
uint private randNonce = 0;
uint private modulus = 1000;
mapping(uint256=>bool) pendingRequests;
event GetLatestEthPriceEvent(address callerAddress, uint id);
event SetLatestEthPriceEvent(uint256 ethPrice, address callerAddress);
function getLatestEthPrice() public returns (uint256) {
randNonce++;
uint id = uint(keccak256(abi.encodePacked(now, msg.sender, randNonce))) % modulus;
pendingRequests[id] = true;
emit GetLatestEthPriceEvent(msg.sender, id);
return id;
}
// Start here
function setLatestEthPrice(uint256 _ethPrice, address _callerAddress, uint256 _id) public onlyOwner {
require(pendingRequests[_id], "This request is not in my pending list.");
delete pendingRequests[_id];
}
}
接下來還有以下流程需要處理
pragma solidity 0.5.0;
import "openzeppelin-solidity/contracts/ownership/Ownable.sol";
import "./CallerContractInterface.sol";
contract EthPriceOracle is Ownable {
uint private randNonce = 0;
uint private modulus = 1000;
mapping(uint256=>bool) pendingRequests;
event GetLatestEthPriceEvent(address callerAddress, uint id);
event SetLatestEthPriceEvent(uint256 ethPrice, address callerAddress);
function getLatestEthPrice() public returns (uint256) {
randNonce++;
uint id = uint(keccak256(abi.encodePacked(now, msg.sender, randNonce))) % modulus;
pendingRequests[id] = true;
emit GetLatestEthPriceEvent(msg.sender, id);
return id;
}
function setLatestEthPrice(uint256 _ethPrice, address _callerAddress, uint256 _id) public onlyOwner {
require(pendingRequests[_id], "This request is not in my pending list.");
delete pendingRequests[_id];
// Start here
CallerContractInterface callerContractInstance;
callerContractInstance = CallerContractInterface(_callerAddress);
callerContractInstance.callback(_ethPrice, _id);
emit SetLatestEthPriceEvent(_ethPrice, _callerAddress);
}
}